/*
Copyright 2022 The Karmada Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
	"fmt"

	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	kubeclient "k8s.io/client-go/kubernetes"

	clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
	"github.com/karmada-io/karmada/pkg/util/names"
)

var (
	clusterResourceKind = clusterv1alpha1.SchemeGroupVersion.WithKind("Cluster")

	// Policy rules allowing full access to resources in the cluster or namespace.
	namespacedPolicyRules = []rbacv1.PolicyRule{
		{
			Verbs:     []string{rbacv1.VerbAll},
			APIGroups: []string{rbacv1.APIGroupAll},
			Resources: []string{rbacv1.ResourceAll},
		},
	}
	// ClusterPolicyRules represents cluster policy rules
	ClusterPolicyRules = []rbacv1.PolicyRule{
		namespacedPolicyRules[0],
		{
			NonResourceURLs: []string{rbacv1.NonResourceAll},
			Verbs:           []string{"get"},
		},
	}
)

type generateClusterInControllerPlaneFunc func(opts ClusterRegisterOption) (*clusterv1alpha1.Cluster, error)

// ObtainCredentialsFromMemberCluster obtain credentials for member cluster
func ObtainCredentialsFromMemberCluster(clusterKubeClient kubeclient.Interface, opts ClusterRegisterOption) (*corev1.Secret, *corev1.Secret, error) {
	var impersonatorSecret *corev1.Secret
	var clusterSecret *corev1.Secret
	var err error
	// It's necessary to set the label of namespace to make sure that the namespace is created by Karmada.
	labels := map[string]string{
		KarmadaSystemLabel: KarmadaSystemLabelValue,
	}
	// ensure namespace where the karmada control plane credential be stored exists in cluster.
	if _, err = EnsureNamespaceExistWithLabels(clusterKubeClient, opts.ClusterNamespace, opts.DryRun, labels); err != nil {
		return nil, nil, err
	}

	if opts.IsKubeImpersonatorEnabled() {
		// create a ServiceAccount for impersonation in cluster.
		impersonationSA := &corev1.ServiceAccount{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: opts.ClusterNamespace,
				Name:      names.GenerateServiceAccountName("impersonator"),
				Labels:    labels,
			},
		}
		if impersonationSA, err = EnsureServiceAccountExist(clusterKubeClient, impersonationSA, opts.DryRun); err != nil {
			return nil, nil, err
		}

		impersonatorSecret, err = WaitForServiceAccountSecretCreation(clusterKubeClient, impersonationSA)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get serviceAccount secret for impersonation from cluster(%s), error: %v", opts.ClusterName, err)
		}
	}
	if opts.IsKubeCredentialsEnabled() {
		// create a ServiceAccount in cluster.
		serviceAccountObj := &corev1.ServiceAccount{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: opts.ClusterNamespace,
				Name:      names.GenerateServiceAccountName(opts.ClusterName),
				Labels:    labels,
			},
		}
		if serviceAccountObj, err = EnsureServiceAccountExist(clusterKubeClient, serviceAccountObj, opts.DryRun); err != nil {
			return nil, nil, err
		}

		// create a ClusterRole in cluster.
		clusterRole := &rbacv1.ClusterRole{
			ObjectMeta: metav1.ObjectMeta{
				Name:   names.GenerateRoleName(serviceAccountObj.Name),
				Labels: labels,
			},
			Rules: ClusterPolicyRules,
		}
		if _, err = EnsureClusterRoleExist(clusterKubeClient, clusterRole, opts.DryRun); err != nil {
			return nil, nil, err
		}

		// create a ClusterRoleBinding in cluster.
		clusterRoleBinding := &rbacv1.ClusterRoleBinding{
			ObjectMeta: metav1.ObjectMeta{
				Name:   clusterRole.Name,
				Labels: labels,
			},
			Subjects: BuildRoleBindingSubjects(serviceAccountObj.Name, serviceAccountObj.Namespace),
			RoleRef:  BuildClusterRoleReference(clusterRole.Name),
		}
		if _, err = EnsureClusterRoleBindingExist(clusterKubeClient, clusterRoleBinding, opts.DryRun); err != nil {
			return nil, nil, err
		}
		clusterSecret, err = WaitForServiceAccountSecretCreation(clusterKubeClient, serviceAccountObj)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to get serviceAccount secret from cluster(%s), error: %v", opts.ClusterName, err)
		}
	}
	if opts.DryRun {
		return nil, nil, nil
	}
	return clusterSecret, impersonatorSecret, nil
}

// RegisterClusterInControllerPlane represents register cluster in controller plane
func RegisterClusterInControllerPlane(opts ClusterRegisterOption, controlPlaneKubeClient kubeclient.Interface, generateClusterInControllerPlane generateClusterInControllerPlaneFunc) error {
	// It's necessary to set the label of namespace to make sure that the namespace is created by Karmada.
	labels := map[string]string{
		KarmadaSystemLabel: KarmadaSystemLabelValue,
	}
	// ensure namespace where the cluster object be stored exists in control plane.
	if _, err := EnsureNamespaceExistWithLabels(controlPlaneKubeClient, opts.ClusterNamespace, opts.DryRun, labels); err != nil {
		return err
	}

	impersonatorSecret := &corev1.Secret{}
	secret := &corev1.Secret{}
	var err error

	if opts.IsKubeImpersonatorEnabled() {
		// create secret to store impersonation info in control plane
		impersonatorSecret = &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: opts.ClusterNamespace,
				Name:      names.GenerateImpersonationSecretName(opts.ClusterName),
				Labels:    labels,
			},
			Data: map[string][]byte{
				clusterv1alpha1.SecretTokenKey: opts.ImpersonatorSecret.Data[clusterv1alpha1.SecretTokenKey],
			},
		}
		impersonatorSecret, err = CreateSecret(controlPlaneKubeClient, impersonatorSecret)
		if err != nil {
			return fmt.Errorf("failed to create impersonator secret in control plane. error: %v", err)
		}
		opts.ImpersonatorSecret = *impersonatorSecret
	}

	if opts.IsKubeCredentialsEnabled() {
		secret = &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: opts.ClusterNamespace,
				Name:      opts.ClusterName,
				Labels:    labels,
			},
			Data: map[string][]byte{
				clusterv1alpha1.SecretCADataKey: opts.Secret.Data["ca.crt"],
				clusterv1alpha1.SecretTokenKey:  opts.Secret.Data[clusterv1alpha1.SecretTokenKey],
			},
		}

		secret, err = CreateSecret(controlPlaneKubeClient, secret)
		if err != nil {
			return fmt.Errorf("failed to create secret in control plane. error: %v", err)
		}
		opts.Secret = *secret
	}
	cluster, err := generateClusterInControllerPlane(opts)
	if err != nil {
		return err
	}
	patchSecretBody := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			OwnerReferences: []metav1.OwnerReference{
				*metav1.NewControllerRef(cluster, clusterResourceKind),
			},
		},
	}
	if opts.IsKubeImpersonatorEnabled() {
		err = PatchSecret(controlPlaneKubeClient, impersonatorSecret.Namespace, impersonatorSecret.Name, types.MergePatchType, patchSecretBody)
		if err != nil {
			return fmt.Errorf("failed to patch impersonator secret %s/%s, error: %v", impersonatorSecret.Namespace, impersonatorSecret.Name, err)
		}
	}

	if opts.IsKubeCredentialsEnabled() {
		err = PatchSecret(controlPlaneKubeClient, secret.Namespace, secret.Name, types.MergePatchType, patchSecretBody)
		if err != nil {
			return fmt.Errorf("failed to patch secret %s/%s, error: %v", secret.Namespace, secret.Name, err)
		}
	}
	return nil
}
